////////////////////////////////////////////////
// DESCRIPTION:
//    Support classes for creating MSI Custom Actions
//
// Legal Notices:
//   Copyright (c) 2008, Telliam Consulting, LLC.
//   All rights reserved.
//
//   Redistribution and use in source and binary forms, with or without modification,
//   are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice, this list
//     of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright notice, this
//     list of conditions and the following disclaimer in the documentation and/or other
//     materials provided with the distribution.
//   * Neither the name of Telliam Consulting nor the names of its contributors may be
//     used to endorse or promote products derived from this software without specific
//     prior written permission.
//
//   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
//   EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
//   OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
//   SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
//   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
//   TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
//   BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
//   CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
//   ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
//   DAMAGE. 
//

#include "StdAfx.h"
#include "CustomActionHelpers.h"

static const TCHAR* DirctoryOfFileQuery = _T("SELECT `Directory` ")
                                          _T(" FROM `File`,`Directory`,`Component`")
                                          _T(" WHERE `Component_`=`Component`")
                                          _T(" AND `Directory_`=`Directory`")
                                          _T(" AND `File`='%s'");


static const TCHAR* FileNameFromIdQuery = _T("SELECT `FileName` ")
                                          _T(" FROM `File`")
                                          _T(" WHERE `File`='%s'");

using namespace FusionWare::MSI;

UINT Session::ProcessMessage(INSTALLMESSAGE MessageType, Record Message)
{
	UINT err = ::MsiProcessMessage(this->hInstall, MessageType, Message);
	if( IDCANCEL == err)
		err = ERROR_INSTALL_USEREXIT;

    return err;
}

void Session::LogMsg(const TCHAR* Fmt,...)
{
    va_list argptr;
    CString msgTxt;
    PMSIHANDLE hRecord = MsiCreateRecord(1);

    va_start(argptr, Fmt);

    msgTxt.FormatV(Fmt, argptr);

    ::MsiRecordSetString(hRecord, 0, msgTxt);

    UINT er = ::MsiProcessMessage(this->hInstall, INSTALLMESSAGE_INFO, hRecord);
	if((ERROR_INSTALL_USEREXIT == er || IDCANCEL == er))
        throw FusionWare::MSI::Exception(_T("Log ERROR"), er);
}

void Session::ExtendProgressBar(UINT Cost)
{
	Record hRec((UINT)3);

    hRec.SetField( 1, 3 );
    hRec.SetField( 2, Cost);
    hRec.SetField( 3, 0);

    UINT err = ProcessMessage(INSTALLMESSAGE_PROGRESS, hRec);
	if (0 == err || IDOK == err || IDYES == err)
		return;
   
    if (IDABORT == err || IDCANCEL == err)
        throw FusionWare::MSI::Exception(_T("User Exit"), ERROR_INSTALL_USEREXIT);
	else
        throw FusionWare::MSI::Exception(_T("Unexpected error in Extending progress bar"), E_UNEXPECTED);
}

CString Session::GetProperty(const TCHAR* Name)
{
    CString retVal;
    DWORD cchValueBuf = 0;
    UINT err =  MsiGetProperty(this->hInstall, Name, _T(""), &cchValueBuf);
    if( ERROR_MORE_DATA == err )
    {
        ++cchValueBuf; // on output does not include terminating null, so add 1
        TCHAR* pBuf = retVal.GetBufferSetLength((int)cchValueBuf);
        if(pBuf == NULL)
            throw FusionWare::MSI::OutOfMemory;

        err = MsiGetProperty(this->hInstall, Name, pBuf, &cchValueBuf);
        retVal.ReleaseBuffer();
    }
    ThrowOnFailure(err, _T("Could not retrieve property data"));
    
    return retVal;
}

void Session::SetProperty(const TCHAR* Name, const TCHAR* Value)
{
    ThrowOnFailure( ::MsiSetProperty(this->hInstall, Name, Value)
                  , _T("Could not set property")
                  );
}


CPath Session::GetTargetPath(const TCHAR *DirName)
{
    CString retVal;
    DWORD cchValueBuf = 0;
    UINT err = MsiGetTargetPath(this->hInstall, DirName, _T(""), &cchValueBuf);
    if( ERROR_MORE_DATA == err )
    {
        ++cchValueBuf; // on output does not include terminating null, so add 1
        TCHAR* pBuf = retVal.GetBufferSetLength((int)cchValueBuf);
        if(pBuf == NULL)
            throw FusionWare::MSI::OutOfMemory;

        err = MsiGetTargetPath(this->hInstall, DirName, pBuf, &cchValueBuf);
        retVal.ReleaseBuffer();
    }
    ThrowOnFailure(err, _T("Could not retrieve property data"));
    
    return CPath(retVal);
}

CString Session::GetFileName(const TCHAR* FileId)
{
    // query for the Filename matching the FileId
    CString fileName;
    std::auto_ptr<View> results = CreateAndExecuteView(FileNameFromIdQuery, FileId);
    std::auto_ptr<Record> result0 = results->Fetch();
    result0->GetField(1, fileName);

    // get name from record....
    std::vector<CString> names = SplitString(fileName, _T("|"));
    if( names.size() > 1 )
        return names[1];
    else
        return names[0];
}

CPath Session::GetFileInstallPath(const TCHAR* FileId)
{
    // run query to get directory name
    CString dirName;   
    std::auto_ptr<View> results = CreateAndExecuteView(DirctoryOfFileQuery, FileId);
    std::auto_ptr<Record> result0 = results->Fetch();
    result0->GetField(1, dirName);

    CPath dir = GetTargetPath(dirName);
    dir.Append(GetFileName(FileId));
    return dir;
}


// replacement for CString::Tokenize that will not skip leading delimiters
CString Tokenize( const CString& Txt, const TCHAR* pszTokens, _Inout_ int& iStart )
{
	ATLASSERT( iStart >= 0 );
		
	if(iStart < 0)
		AtlThrow(E_INVALIDARG);			
		
	if( (pszTokens == NULL) || (*pszTokens == (TCHAR)0) )
	{
		if (iStart < Txt.GetLength())
			return( CString( Txt.GetString()+iStart, Txt.GetManager() ) );
	}
	else
	{
		const TCHAR* pszPlace = Txt.GetString()+iStart;
		const TCHAR* pszEnd = Txt.GetString()+Txt.GetLength();
		if( pszPlace < pszEnd )
		{
			if( pszPlace < pszEnd )
			{
                int nExcluding = CString::StrTraits::StringSpanExcluding( pszPlace, pszTokens );

				int iFrom = iStart;
				int nUntil = nExcluding;
				iStart = iFrom + nUntil + 1;

				return( Txt.Mid( iFrom, nUntil ) );
			}
		}
	}

	// return empty string, done tokenizing
	iStart = -1;

	return CString( Txt.GetManager() );
}


std::vector<CString> Session::SplitString(const CString& String, const TCHAR* pDelimiter)
{
    std::vector<CString> retVal;

    // split the string based on the delimiter(s)
    for(int start=0; start < String.GetLength(); )
        retVal.push_back( Tokenize(String, pDelimiter, start) );

    const TCHAR* pLastChar = String.GetString() + String.GetLength()-1;
    if( 0!= CString::StrTraits::StringSpanIncluding(pLastChar , pDelimiter))
        retVal.push_back( CString() );

    return retVal;
}

std::vector<CString> Session::GetCustomActionData(unsigned MinArgs, const TCHAR* pDelimiter)
{
    std::vector<CString> argv;
    CString customData = GetProperty(_T("CustomActionData"));

    //  split the string based on the delimiter
    argv = SplitString(customData, pDelimiter);

    if( MinArgs > 0  && argv.size() < MinArgs )
        throw FusionWare::MSI::Exception(_T("Incorrect number of arguments found in CustomActionData!"), ERROR_BAD_ARGUMENTS);

    return argv;
}

UINT Session::DoCustomAction(CustomAction Action)
{
    try 
    {
        Action();
        return ERROR_SUCCESS;
    }
    catch( FusionWare::MSI::Exception e )
    {
        LogMsg(e.Msg, false);
        return e.Error;
    }
    catch( std::bad_alloc e )
    {
        LogMsg(_T("Out of Memory"),false);
        return ERROR_NOT_ENOUGH_MEMORY;
    }
    catch( _com_error e )
    {
        _bstr_t desc = e.Description();
        LogMsg(desc, false); // block exception on fail to log
        return e.Error();
    }
}

//std::auto_ptr<View> Session::CreateAndExecuteView(LPCTSTR pQuery, Record Params)
//{
//    MSIHANDLE hView;
//
//    ThrowOnFailure( ::MsiDatabaseOpenView(this->hDataBase, pQuery, &hView)
//                  , _T("Failed to open database view")
//                  );
//    
//    // make sure this is released in the event of a failure below...
//    std::auto_ptr<View> retVal(new View(hView));
//
//    ThrowOnFailure( ::MsiViewExecute(hView, Params)
//                  , _T("Failed to execute view")
//                  );
//    
//    return retVal;
//}

std::auto_ptr<View> Session::CreateAndExecuteView(CString Format, ...)
{
    CString fullQuery;
    MSIHANDLE hView;
    va_list args;
    va_start(args, Format);

    fullQuery.FormatV(Format, args);
    ThrowOnFailure( ::MsiDatabaseOpenView(this->hDataBase, fullQuery, &hView)
                  , _T("Failed to open database view")
                  );
    
    // make sure this is released in the event of a failure below...
    std::auto_ptr<View> retVal(new View(hView));

    ThrowOnFailure( ::MsiViewExecute(hView, NULL)
                  , _T("Failed to execute view")
                  );

    return retVal;
}

void Session::ScheduleDeferredAction(const TCHAR* Name, const TCHAR* CustomActionData, UINT Cost)
{
	if( CustomActionData && *CustomActionData)
		SetProperty(Name, CustomActionData);

	if( 0 < Cost)
		ExtendProgressBar(Cost);

    ThrowOnFailure( ::MsiDoActionW(this->hInstall, Name)
                  , CString(_T("Failed MsiDoAction for deferred action ")) + Name
                  );
}

CPath Session::CreateCustomActionLogName(LogAction Action, LogType Type, bool Impersonated)
{
    CString fileName;
    CPath retVal;

    TCHAR scheduledOrRollback = Type == LogTypeScheduled ? _T('s') : _T('r');
    TCHAR userOrMachine = Impersonated ? _T('u') : _T('m');
    TCHAR installOrUninstall = Action == LogActionInstall ? _T('i') : _T('u');

    fileName.Format( _T("FWMSI%s.%s.%c%c%c")
                   , GetProperty(_T("ProductCode"))
                   , this->LogName
                   , scheduledOrRollback
                   , userOrMachine
                   , installOrUninstall
                   );
    
    TCHAR tempPath[MAX_PATH];

    if( Impersonated )
    {
        if (!::GetTempPath(MAX_PATH, tempPath))
            throw FusionWare::MSI::Exception(_T("Failed to get temp path."), GetLastError());
        
        retVal.Combine(tempPath,_T(""));
    }
    else
    {
        if (!::GetWindowsDirectory(tempPath, MAX_PATH))
            throw FusionWare::MSI::Exception(_T("Failed to get windows path."), GetLastError());

        retVal.Combine(tempPath, _T("Installer"));
    }

    retVal.Append(fileName);
    return retVal;
}

std::auto_ptr<Record> View::Fetch()
{
    // don't use PMSIHANDLE here so the handle isn't
    // closed on scope exit. That's covered by the
    // returned record type.
    MSIHANDLE hRecord;

    ThrowOnFailure( ::MsiViewFetch(this->hView, &hRecord)
                  , _T("Failed to fetch record from view")
                  );

    return std::auto_ptr<Record>(new Record(hRecord));
}

void View::ForEachRecord(RecordHandler Action)
{
    Record record;

    for( UINT err = ::MsiViewFetch(this->hView, record); err != ERROR_NO_MORE_ITEMS; err = ::MsiViewFetch(this->hView, record))
    {
        ThrowOnFailure(err, _T("Error retrieving record"));
        Action(record);
    }
}

void Record::GetField(UINT FieldIndex, int& Value, int DefaultValue /*=0*/)
{
    Value = ::MsiRecordGetInteger(this->hRecord, FieldIndex);
    if(Value == MSI_NULL_INTEGER)
        Value = DefaultValue;
}

void Record::GetField(UINT FieldIndex, CString& Value, bool DefaultSpace /* = true */)
{
    DWORD cchValueBuf = 0;
    UINT err =  ::MsiRecordGetString(this->hRecord, FieldIndex, _T(""), &cchValueBuf);
    if( ERROR_MORE_DATA == err )
    {
        ++cchValueBuf; // on output does not include terminating null, so add 1
        TCHAR* pBuf = Value.GetBufferSetLength((int)cchValueBuf);
        if(pBuf == NULL)
            throw FusionWare::MSI::OutOfMemory;

        err = ::MsiRecordGetString(this->hRecord, FieldIndex, pBuf, &cchValueBuf);
        Value.ReleaseBuffer();
    }
    ThrowOnFailure(err, _T("Could not retrieve string field"));
}

void Record::SetField(UINT FieldIndex, CString& Value)
{
    ThrowOnFailure( ::MsiRecordSetString(this->hRecord, FieldIndex, Value)
                  , _T("Could not set string field")
                  );
}

void Record::SetField(UINT FieldIndex, int Value)
{
    ThrowOnFailure( ::MsiRecordSetInteger(this->hRecord, FieldIndex, Value)
                  , _T("Could not set string field")
                  );
}

void CALogFile::Create(const CPath& Path)
{
    hFile = ::CreateFile(Path, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
    if(INVALID_HANDLE_VALUE == hFile)
        throw FusionWare::MSI::Exception(_T("Could not open CA log file!"), HRESULT_FROM_WIN32(GetLastError())); 

    this->DeleteOnClose = false;
}

void CALogFile::Open(const CPath& Path, bool DeleteOnClose)
{
    if(!Path.FileExists())
        throw FusionWare::MSI::Exception(_T("CA log file does not exist!"), HRESULT_FROM_WIN32(GetLastError()));
    
    this->hFile = ::CreateFile(Path, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
    ThrowLastErrorOnFailure(INVALID_HANDLE_VALUE != this->hFile, _T("Could not open CA log file!")); 

    this->DeleteOnClose = DeleteOnClose;
    if(DeleteOnClose)
        this->Path = Path;
}

void CALogFile::Close()
{
    if(INVALID_HANDLE_VALUE != this->hFile)
        ::CloseHandle(this->hFile);

    if(this->DeleteOnClose)
        ::DeleteFile(this->Path);
}

void CALogFile::Write(const CString& Txt )
{
    DWORD numWritten;
    ThrowLastErrorOnFailure( ::WriteFile(this->hFile, (LPCTSTR)Txt, Txt.GetLength() * sizeof(TCHAR), &numWritten, NULL)
                           , _T("Failed to write to file")
                           );
}

CString CALogFile::Read()
{
    CString retVal;
    LARGE_INTEGER fileSize = { 0 };

    ThrowLastErrorOnFailure( ::GetFileSizeEx(this->hFile, &fileSize)
                           , _T("Failed to get size of ca log file.")
                           );

    if (0 != fileSize.HighPart || 0 != (fileSize.LowPart % sizeof(TCHAR)))
    {
        throw FusionWare::MSI::Exception( _T("Invalid data read from ca log file.")
                                        , HRESULT_FROM_WIN32(ERROR_INVALID_DATA)
                                        );
    }
    
    ThrowLastErrorOnFailure( INVALID_SET_FILE_POINTER != ::SetFilePointer(this->hFile, 0, NULL, FILE_BEGIN)
                           , _T("Failed to reset to beginning of ca log file.")
                           );

    int len = fileSize.LowPart / sizeof(TCHAR);
    BYTE* pData = (BYTE*)retVal.GetBufferSetLength( len );
    if( !pData )
        throw FusionWare::MSI::OutOfMemory;

    DWORD totalRead = 0;
    DWORD numRead = 0;
    do
    {
        ThrowLastErrorOnFailure( ::ReadFile(this->hFile, pData + totalRead, fileSize.LowPart - totalRead, &numRead, NULL)
                               , _T("Failed to read from ca log file.")
                               );

        totalRead += numRead;
    } while( numRead && totalRead < fileSize.LowPart);

    if( totalRead != fileSize.LowPart)
        throw FusionWare::MSI::Exception(_T("Failed to completely read ca log file."), E_UNEXPECTED);

    retVal.ReleaseBuffer();
    return retVal;
}
